package com.eastfair.gateway.filters;

import cn.hutool.Hutool;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.api.config.annotation.NacosValue;
import com.eastfair.cache.redis.RedisOps;
import com.eastfair.core.base.R;
import com.eastfair.core.context.ContextConstants;
import com.eastfair.core.context.ContextUtil;
import com.eastfair.core.exception.BizException;
import com.eastfair.core.exception.code.ExceptionCode;
import com.eastfair.core.utils.HashStrUtil;
import com.eastfair.core.utils.HumpUtil;
import com.eastfair.handler.EfTokenHandler;
import com.eastfair.jwt.model.AuthInfo;
import com.eastfair.jwt.service.JwtService;
import com.eastfair.properties.IgnoreProperties;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.units.qual.A;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * 鉴权过滤器
 *
 * @Author ligang
 * @Date 2020/3/16 23:33
 */
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {
    @Autowired
    JwtService jwtService;

    @Autowired
    EfTokenHandler efTokenHandler;

    @Autowired
    private RedisOps redisOps;

    @Value("#{'${url-ignore.list:}'.empty ? null : '${url-ignore.list:}'.split(' ')}")
//    @Value("#{'${url-ignore.list:}'}")
    private List<String> urlignoreList;


//    @NacosValue(value = "${gatewayUrlIgnore.json}", autoRefreshed = true)
//    private String baseUri;

    /**
     * 设置webflux模型响应
     *
     * @param response    ServerHttpResponse
     * @param contentType content-type
     * @param status      http状态码
     * @param value       响应内容
     * @return Mono<Void>
     */
    private static Mono<Void> buildResponse(ServerHttpResponse response, String contentType,
                                            HttpStatus status, Object value) {
        response.setStatusCode(status);
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, contentType);
        DataBuffer dataBuffer = response.bufferFactory().wrap(JSONObject.toJSONString(value).getBytes());
        return response.writeWith(Mono.just(dataBuffer));
    }

    public Mono<Void> assembleExchange(ServerWebExchange exchange, GatewayFilterChain chain, String token) {
        ServerHttpResponse response = exchange.getResponse();
        ServerHttpRequest request = exchange.getRequest();
        String requestUrl = request.getPath().toString();
        String requestMethod = request.getMethodValue();
        if (HttpMethod.POST.toString().equals(requestMethod) || HttpMethod.PUT.toString().equals(requestMethod)) {
            return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                String postRequestBodyStr = new String(bytes, StandardCharsets.UTF_8);
                exchange.getAttributes().put("POST_BODY", postRequestBodyStr);
                DataBufferUtils.release(dataBuffer);
                Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
                    DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                    return Mono.just(buffer);
                });
                // 下面的将请求体再次封装写回到request里，传到下一级，否则，由于请求体已被消费，后续的服务将取不到值
                ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                    @Override
                    public Flux<DataBuffer> getBody() {
                        return cachedFlux;
                    }
                };
                Boolean isAgain = submitAgain(requestUrl, requestMethod, postRequestBodyStr, token);
//                if (isAgain) {
//                    // 封装错误信息
//                    Map<String, Object> responseData = Maps.newHashMap();
//                    responseData.put("code", ExceptionCode.SUBMIT_AGAIN.getCode());
//                    responseData.put("date", ExceptionCode.SUBMIT_AGAIN.getMsg());
////                    responseData.put("cause", "submit is again");
//                    // 输出错误信息到页面
//                    return responseMsgOfException(responseData, response);
//                }
                // 封装request，传给下一级
                Mono<Void> mono = chain.filter(exchange.mutate().request(mutatedRequest).build());
                return mono;
            });
        } else if (HttpMethod.GET.toString().equals(requestMethod)
                || HttpMethod.DELETE.toString().equals(requestMethod)) {
            MultiValueMap<String, String> getRequestParams = request.getQueryParams();
            log.debug("\n 请求url:`{}` \n 请求类型：{} \n 请求参数：{}", requestUrl, requestMethod, getRequestParams);
            Boolean isAgain = submitAgain(requestUrl, requestMethod, JSON.toJSONString(getRequestParams), token);
//            if (isAgain) {
//                // 封装错误信息
////                Map<String, Object> responseData = Maps.newHashMap();
////                responseData.put("code", 401);
////                responseData.put("message", "重复提交");
////                responseData.put("cause", "submit is again");
//                Map<String, Object> responseData = Maps.newHashMap();
//                responseData.put("code", ExceptionCode.SUBMIT_AGAIN.getCode());
//                responseData.put("data", ExceptionCode.SUBMIT_AGAIN.getMsg());
//                // 输出错误信息到页面
//                return responseMsgOfException(responseData, response);
//            }
            Mono<Void> mono = chain.filter(exchange);
            return mono;
        }
        return chain.filter(exchange);
    }


    /**
     * 判断是否重复提交
     *
     * @param requestUrl
     * @param requestMethod
     * @param requestParams
     * @param token
     * @return
     */
    public Boolean submitAgain(String requestUrl, String requestMethod, String requestParams, String token) {
        List<String> baseUri = Arrays.asList(
                "/project-core/"
        );
        for (String uri : baseUri) {
            //接口不需要重复提交
            if (requestUrl.indexOf(uri) > -1) {
                return false;
            }
        }
        ContextUtil.setGatewayUrl(requestUrl);
        ContextUtil.setGatewayMethod(requestMethod);
        ContextUtil.setGatewayParams(requestParams);
        ContextUtil.setGatewayToken(token);
        ContextUtil.setGatewayCreateTime(System.currentTimeMillis());

        if (StringUtils.isBlank(token)) {
            return false;
        }
        String keyType = "submitkey:";
        String key = keyType + HashStrUtil.getMiscHashCode(requestUrl + requestMethod + requestParams + token);
        if (redisOps.exists(key)) {
            return true;
        }
        redisOps.setEx(key, "1", 1, false);
        return false;
    }


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info(JSONObject.toJSONString(urlignoreList));
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        ServerHttpRequest.Builder mutate = request.mutate();

        String requestUrl = request.getPath().toString();
        String requestMethod = request.getMethodValue();
        addHeader(mutate, ContextConstants.GATEWAY_RQUEST_URL, requestUrl);
        addHeader(mutate, ContextConstants.GATEWAY_RQUEST_METHOD, requestMethod);

        //特殊处理，将前端header头里的信息改变名字放到request里，是前端header和body 参数属性名无法保持一致
        loadHeaderOfLeader(mutate, request);
        //灰度发布参数
        String grayVersion = getHeader(ContextConstants.GRAY_VERSION, request);
        ContextUtil.setGrayVersion(grayVersion);
        //链路追踪id
        assembleTraceId(mutate);
        // 判断接口是否需要忽略token验证
        if (isSkipPath(request.getPath().toString())) {
//            ContextUtil.setIsIgnoreUrl(1);
//            addHeader(mutate, ContextConstants.IS_IGNORE_URL, 1);
            log.debug("当前接口：{}, 不解析用户token", request.getPath().toString());
//            return chain.filter(exchange);
            //组装 http的header值
            AuthInfo authInfo = new AuthInfo();
            String projectid = getHeader(ContextConstants.JWT_KEY_PROJECT_ID, request);
            if (StringUtils.isNotBlank(projectid)) {
                authInfo.setProjectId(Convert.toLong(projectid));
            }
            authInfo.setSystemScene(getHeader(ContextConstants.SYSTEM_SCENE, request));

            assembleOfHeader(mutate, authInfo);
            log.debug("项目id：{}", projectid);
            return assembleExchange(exchange, chain, null);
        }

        // 获取请求头中的token
        String token = getHeader(ContextConstants.BEARER_HEADER_KEY, request);
        log.info("token is {}", token);
        if (token == null || token.isEmpty() || !token.startsWith("Bearer ")) {
            // 封装错误信息
            Map<String, Object> responseData = Maps.newHashMap();
            responseData.put("code", 401);
            responseData.put("message", "非法请求");
            responseData.put("cause", "Token is empty");
            // 输出错误信息到页面
            return responseMsg(responseData, response);
        }

        token = token.replace("Bearer ", "");
        AuthInfo authInfo = null;
        try {
            authInfo = jwtService.parse(token);
//            if (!isTokenClash(token, request, authInfo)) {
//                // 封装错误信息
//                Map<String, Object> responseData = Maps.newHashMap();
//                responseData.put("code", 403);
//                responseData.put("message", "token冲突，请重新登录");
//                responseData.put("cause", "Token is clash");
//                // 输出错误信息到页面
//                return responseMsg(responseData,response);
//            }
        } catch (BizException e) {
            return errorResponse(response, e.getMessage(), e.getCode(), HttpStatus.UNAUTHORIZED);
        } catch (Exception e) {
            e.printStackTrace();
            return errorResponse(response, "验证token出错", R.FAIL_CODE, HttpStatus.UNAUTHORIZED);
        }
        // 将请求头中的token解析出来的用户信息重新封装到请求头，转发到业务服务
        // 业务服务在利用HeaderThreadLocalInterceptor拦截器将请求头中的用户信息解析到ThreadLocal中
        if (authInfo != null) {
            log.info("authInfo is {}", JSONObject.toJSONString(authInfo));
            loadHeaderOfAuthInfo(mutate, authInfo);
        }

        //重要：切换属性，当A系统调用B系统接口时，需要切换到B系统的系统ID，这样数据归属B系统
//        switchHeader(mutate, request);
//        return chain.filter(exchange);
        return assembleExchange(exchange, chain, token);
    }


    /**
     * 组装 链路追踪id
     *
     * @param mutate
     */
    private void assembleTraceId(ServerHttpRequest.Builder mutate) {
        //生成链路追踪id
        String traceId = IdUtil.fastSimpleUUID();
        //存入request header头信息
        addHeader(mutate, ContextConstants.LOG_TRACE_ID, traceId);

        ContextUtil.setTrace(traceId);
    }


    /**
     * 组装 http的header值
     *
     * @param mutate
     */
    private void assembleOfHeader(ServerHttpRequest.Builder mutate, AuthInfo authInfo) {
        //存入request header头信息
        if (authInfo.getProjectId() != null) {
            addHeader(mutate, ContextConstants.JWT_KEY_PROJECT_ID, authInfo.getProjectId());
        }
        if (StringUtils.isNotBlank(authInfo.getSystemScene())) {
            addHeader(mutate, ContextConstants.SYSTEM_SCENE, authInfo.getSystemScene());
        }
    }


    /**
     * 返回错误信息
     *
     * @param responseData
     * @param response
     * @return
     */
    private Mono<Void> responseMsg(Map<String, Object> responseData, ServerHttpResponse response) {
        try {
            // 将信息转换为 JSON
            ObjectMapper objectMapper = new ObjectMapper();
            byte[] data = objectMapper.writeValueAsBytes(responseData);
            // 输出错误信息到页面
            DataBuffer buffer = response.bufferFactory().wrap(data);
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            return response.writeWith(Mono.just(buffer));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 返回异常信息
     *
     * @param responseData
     * @param response
     * @return
     */
    private Mono<Void> responseMsgOfException(Map<String, Object> responseData, ServerHttpResponse response) {
        try {
            // 将信息转换为 JSON
            ObjectMapper objectMapper = new ObjectMapper();
            byte[] data = objectMapper.writeValueAsBytes(responseData);
            // 输出错误信息到页面
            DataBuffer buffer = response.bufferFactory().wrap(data);
            response.setStatusCode(HttpStatus.OK);
            response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            return response.writeWith(Mono.just(buffer));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * token 端是否冲突
     */
    private boolean isTokenClash(String token, ServerHttpRequest request, AuthInfo authInfo) {
        String terminalType = getHeader(ContextConstants.TERMINAL_TYPE, request);
        //查询以前的token是否冲突
        String lastToken = efTokenHandler.findToken(terminalType, authInfo.getUserId());
        if (StringUtils.isNotBlank(lastToken)) {
            if (!lastToken.equals(token)) {
                return false;
            }
        } else {
            //添加到redis
            efTokenHandler.addToken(token, terminalType, authInfo.getUserId());
        }

        return true;
    }

    /**
     * 将用户信息加载到request header头里
     *
     * @param mutate
     * @param authInfo
     */
    private void loadHeaderOfAuthInfo(ServerHttpRequest.Builder mutate, AuthInfo authInfo) {
        addHeader(mutate, ContextConstants.JWT_KEY_ACCOUNT, authInfo.getAccount());
        addHeader(mutate, ContextConstants.JWT_KEY_USER_ID, authInfo.getUserId());
        addHeader(mutate, ContextConstants.JWT_KEY_NAME, authInfo.getName());
        addHeader(mutate, ContextConstants.JWT_KEY_PROJECT_ID, authInfo.getProjectId());
        addHeader(mutate, ContextConstants.JWT_KEY_SUBSYSTEM_ID, authInfo.getSubsystemId());
        addHeader(mutate, ContextConstants.JWT_KEY_TEAM_ID, authInfo.getTeamId());
        addHeader(mutate, ContextConstants.JWT_KEY_SUBJECTTYPE, authInfo.getSubjectType());
        addHeader(mutate, ContextConstants.SYSTEM_SCENE, authInfo.getSystemScene());
        addHeader(mutate, ContextConstants.JWT_KEY_REAL_NAME, authInfo.getRealName());
        addHeader(mutate, ContextConstants.JWT_KEY_IS_ADMIN, authInfo.getIsAdmin());
        addHeader(mutate, ContextConstants.JWT_KEY_IS_SUPER_ADMIN, authInfo.getIsSuperAdmin());
    }

    /**
     * 特殊处理，将前端header头里的信息改变名字放到request里，是前端header和body 参数属性名无法保持一致
     *
     * @param mutate
     * @param request
     */
    private void loadHeaderOfLeader(ServerHttpRequest.Builder mutate, ServerHttpRequest request) {
        //需要转驼峰的值
        List<String> headerParamList = Arrays.asList(ContextConstants.EXHIBITION_ID, ContextConstants.LANGUAGE_TYPE,
                ContextConstants.TERMINAL_DEVICE, ContextConstants.TERMINAL_TYPE, ContextConstants.SUBJECT_TYPE, ContextConstants.JWT_KEY_PROJECT_ID,
                ContextConstants.JWT_KEY_SUBSYSTEM_ID, ContextConstants.SYSTEM_SCENE,
                ContextConstants.GATEWAY_RQUEST_URL, ContextConstants.GATEWAY_RQUEST_METHOD);
        for (String s : headerParamList) {
            String value = getHeader(HumpUtil.humpToLine2(s), request);
            if (StringUtils.isNotBlank(value)) {
                addHeader(mutate, s, value);
            }
        }
        //暂时写死projectid
//        mutate.header(ContextConstants.JWT_KEY_PROJECT_ID, projectId+"");
    }


    /**
     * 特殊处理，切换属性，慎用。场景：A系统调用基础服务给B系统创建数据，那系统id需要切换
     *
     * @param mutate
     * @param request
     */
    private void switchHeader(ServerHttpRequest.Builder mutate, ServerHttpRequest request) {
        //需要转驼峰的值
//        List<String> headerParamList = Arrays.asList(ContextConstants.SWITCH_SUBSYSTEM_ID);
//        for (String s : headerParamList) {
//            String value = getHeader(HumpUtil.humpToLine2(s), request);
//            if (StringUtils.isNotBlank(value)) {
//                if (ContextConstants.SWITCH_SUBSYSTEM_ID.equals(s)) {
//                    //切换系统id
//                    addHeader(mutate, ContextConstants.JWT_KEY_SUBSYSTEM_ID, value);
//                }
//            }
//        }
    }


    private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {
        if (value == null) {
            return;
        }
        String valueStr = value.toString();
        String valueEncode = URLUtil.encode(valueStr);
        mutate.header(name, valueEncode);
    }

    protected String getHeader(String headerName, ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String token = StrUtil.EMPTY;
        if (headers == null || headers.isEmpty()) {
            return token;
        }

        token = headers.getFirst(headerName);

        if (StrUtil.isNotBlank(token)) {
            return token;
        }

        return request.getQueryParams().getFirst(headerName);
    }


    /**
     * 设置过滤器的执行顺序
     *
     * @return
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
//        return Ordered.LOWEST_PRECEDENCE;
    }

    protected Mono<Void> errorResponse(ServerHttpResponse response, String errMsg, int errCode, HttpStatus httpStatus) {
        R tokenError = R.fail(errCode, errMsg);
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.setStatusCode(httpStatus);
        DataBuffer dataBuffer = response.bufferFactory().wrap(tokenError.toString().getBytes());
        return response.writeWith(Mono.just(dataBuffer));
    }


    /**
     * @desc 校验url是否跳过
     * @author ZZZ
     * @date 2019/2/20 0020
     * @version 1.0
     */
    private boolean isSkipPath(String uri) {
        Boolean pan = true;
        pan = IgnoreProperties.isIgnoreToken(uri);
        if (!pan) {
            pan = isIgnoreToken(uri);
        }
        return pan;
    }

    public boolean isIgnoreToken(String path) {
        log.info("urlignoreList is {}", JSONObject.toJSONString(urlignoreList));
        if (CollectionUtil.isEmpty(urlignoreList)) {
            return false;
        }
        return urlignoreList.stream().anyMatch(url -> (path.startsWith(url) && IgnoreProperties.isEnd(path, url)) || new AntPathMatcher().match(url, path));
    }

}
